How does it work
-
Ben Eater - What is the Stack and how does it work? 6605 Part 5 .
-
Great video, but it's a bit more about electronics and not so much about assembly code.
-
Story of the problem in the video:
-
He creates subroutines to improve the code readability and size (
jsr/rts) but the "hello world" code breaks. He steps into the code with an Arduino Mega, going one clock at a time to see what's happening. There was some "unexpected" instructions before calling the jump, so when returning from the subroutine, the address to continue is saved. All good. The problem was when returning from the subroutine withrts, the saved address wasn't actually saved; the problem was that there was simply no hardware saving the address. -
Rationale:
-
He could have pushed the "A register" onto the stack (
pha) in the beginning of the subroutine, and pulling the "A register" off the stack (pla) at the end of the subroutine.-
This is not necessary, only if you want to preserve the contents of the
ldaregister.
-
-
He only solved the problem in the next video, by adding hardware to the computer, so the address is saved. Currently there's only ROM (read-only memory).
-
-
-
-
Intermation - Stack Pointer, Increment/Decrement, Push/Pop
-
Shows how Push/Pop work internally, creating copies of the values in the registers and advancing the SP.
-
-
Intermation - Using Push/Pop in the Instruction Pointer to call and return from functions
-
Great video.
-
Reserving Space
-
At the lowest level, there is no “array construct” at all. There are only:
-
a stack pointer adjustment
-
address arithmetic
-
-
Everything else is syntax layered on top.
-
What “reserving space” really is:
-
sub rsp, 32
-
-
The stack pointer (
rsp) just moves down, and that region is now “owned”. -
Example:
MyStruct :: struct { a: u32, data: [16]u8, b: u64, } sizeof(MyStruct) = 32 offset(data) = 4foo: sub rsp, 32 ; reserve entire struct ; x lives at [rsp + 0] ; write x.a = 1 mov dword ptr [rsp + 0], 1 ; write x.data[3] = 0xFF mov byte ptr [rsp + 4 + 3], 0xFF ; write x.b = 42 mov qword ptr [rsp + 24], 42 add rsp, 32 ret-
The compiler lowers:
-
data: [16]u8
-
-
into:
-
+16 bytes at offset 4
-
-
After lowering, the concept of “array” is gone.
-
Only this remains:
base_pointer + constant_offset. -
The:
-
x.data[i]
-
-
Becomes:
-
mov byte ptr [base + 4 + i], ...
-
-
Stack Probing
-
If the struct (and thus array) is large (e.g. > 4KB), compilers may emit stack probing:
sub rsp, 4096
mov [rsp], eax ; touch page
sub rsp, 4096
mov [rsp], eax
-
This prevents skipping guard pages (stack overflow protection).
-
The array itself still doesn’t exist as a concept—only the total size matters.
Scoped Stack Allocation
-
Scoped stack allocation (like alloca)
Introduce something like:
sb := stack_buffer(8)
Rules:
only valid in current scope
cannot escape (compiler enforced)
This is similar to: -
LLVM
alloca -
C
alloca(but unsafe) -
It provides dynamic stack allocation → pointer to memory
-
The core problem is no ownership tracking → easy to create dangling pointers
-
Example:
make_string_buffer :: proc(cap: uint) -> String_Buffer { ptr := stack_alloc(cap) return { ptr, cap, 0 } } sb := make_string_buffer(8)-
If
sbescapes the scope where stack_alloc happened:
return sb // BUG-
bufpoints to invalid stack memory -
This is why languages don’t expose raw “stack allocation returning pointers” freely.
-
Syntaxes Ideas for allocating stack memory
-
data: u8 * N -
data: u8[N] -
data: [N]u8 -
data: stack_alloc(N)-
lowers to
sub rsp, N -
returns a
^u8pointing to that memory
-
-
data: block(u8, N)-
lowers to:
sub rsp, N * sizeof(u8) -
data is just a pointer to the first byte
-
How String Literals are stored
const char *s = "hello";
-
Is stored in the program’s read-only data section:
-
Known at compile time + immutable
-
.rodata-
initialized at compile time
-
never modified at runtime
-
mapped read-only by OS
-
can be shared between processes
-
multiple programs using same library
-
OS maps same physical pages
-
This only works if memory is immutable.
-
-
Size:
-
There is no fixed or intrinsic “capacity” for .rodata.
-
Its size is determined entirely by the program and the platform constraints.
-
"whatever size the compiler/linker emits, limited by the system"
-
Real limits:
-
32-bit systems
-
Typical virtual address space: ~2–4 GB total.
-
So
.rodatamust fit within that along with everything else. -
Large
.rodata(hundreds of MB) can become a problem.
-
-
64-bit systems
-
Typical virtual address space: terabytes+.
-
.rodatais effectively limited by:-
available RAM
-
OS limits
-
-
linker/toolchain constraints
-
Practically:
-
tens or hundreds of MB → fine
-
multi-GB → possible but unusual
-
-
-
-
-
.rodata (ELF / Unix) .rdata (Windows) __TEXT,__cstring (Mach-O / macOS)section .rodata .Lstr: db 'h','e','l','l','o',0 section .text lea rax, [rel .Lstr] ; pointer to string -
-
Static lifetime (exists for entire program execution)
-
Immutable (writing to it is undefined behavior)
char *s = "hello"; s[0] = 'H'; // undefined behavior, because memory is in read-only segment. -
Usually deduplicated by the compiler/linker
char *a = "hello"; char *b = "hello";-
a→ same address asb. Compilers often merge them (string interning at compile time).
-
-
In C:
-
Length is not intrinsic.
-
strlen(s)must scan memory -
O(n) operation.
-
-
Size
-
The stack does have a fixed (or bounded) size, unlike .rodata, and that’s why large allocations can fail.
-
Common defaults
-
Linux: ~8 MB per thread
-
Windows: ~1 MB per thread (default)
-
-
Can be changed, but always finite
-
Why is it limited :
-
Every thread gets its own stack:
-
threads Ă— stack_size = total reserved memory
-
If stacks were large (e.g., 100 MB), creating many threads would explode memory usage.
-
-
Keep it fast:
-
Stack allocation is just:
-
sub rsp, N
-
-
No checks, no allocator.
-
the OS gives a fixed region
-
overflow is detected via guard pages
-
-
Guard pages:
[ stack memory ][ guard page ]-
If you cross it → segmentation fault
-
-
What to do if I want static memory for a critical safe software, but I'm getting blocked by the stack limits the OS gives me?
-
Stop using the stack for large storage and move that memory to a different region with controlled lifetime.
-
The stack is the wrong tool for this requirement.
-
Options :
-
Static/global memory
static uint8_t buffer[4 * 1024 * 1024];-
Lives in
.dataor.bss -
Not limited by stack size
-
Lifetime = entire program
-
No allocation at runtime
-
When to use
-
fixed-size buffers
-
embedded / critical systems
-
known upper bounds
-
-
-
Static Arena, managed by an allocator (no heap usage):
static uint8_t buffer[4 * 1024 * 1024]; typedef struct { uint8_t *ptr; size_t size; size_t used; } Arena;-
deterministic
-
no heap fragmentation
-
no OS allocation after startup
-
flexible usage
-
This is the most common solution in:
-
game engines
-
embedded systems
-
safety-critical code
-
-
-
Heap, but preallocated once.
void *mem = malloc(4 * 1024 * 1024);-
Do this at startup only
-
treat it as a static pool
-
large capacity
-
avoids stack limits
-
still deterministic if controlled
-
-
Increase stack size:
-
Linux:
ulimit -s -
Windows: linker flag
/STACK -
still per-thread
-
still fragile
-
still easy to overflow
-
-
Comparisons :
-
Cache locality:
-
From a cache locality perspective, A (static), B (arena over static), and C (heap preallocated once) are almost identical if they end up as one contiguous block.
-
The differences are subtle and mostly about control and predictability, not raw cache behavior.
-
The cache doesn't care whether memory came from:
-
stack
-
heap
-
.data/.bss
-
-
Once mapped, it’s just virtual memory.
-
-
Determinism:
-
(1)/(2): fixed address range
-
(3): chosen by allocator (less predictable)
-
-
Fragmentation
-
(1)/(2): none
-
(3): none if single allocation
-
-
Startup cost
-
(1)/(2): zero
-
(3): one allocation call
-
-
Binary size
-
(1)/(2)
-
If
.bss(zero-initialized)static uint8_t buffer[4 * 1024 * 1024];-
Binary size: ~0 bytes added (only metadata)
-
The binary just says: "reserve X bytes of zeroed memory"
-
-
If
.data(initialized)static uint8_t buffer[4 * 1024 * 1024] = {1,2,3,...};-
Binary size: +4 MB in executable
-
-
-
(3)
-
Binary size: no increase
-
-
-
RAM Usage
-
(1)/(2)/(3)
-
virtual memory reserved: 4 MB
-
physical memory used: 0 initially
-
Pages are mapped lazily when accessed.
-
file-backed pages
-
loaded on demand
-
-
-
-